home *** CD-ROM | disk | FTP | other *** search
/ Chip 2007 January, February, March & April / Chip-Cover-CD-2007-02.iso / Pakiet bezpieczenstwa / mini Pentoo LiveCD 2006.1 / mpentoo-2006.1.iso / livecd.squashfs / usr / lib / gentoolkit / pym / glsa.py
Text File  |  2006-05-08  |  21KB  |  617 lines

  1. # $Header$
  2.  
  3. # This program is licensed under the GPL, version 2
  4.  
  5. # WARNING: this code is only tested by a few people and should NOT be used
  6. # on production systems at this stage. There are possible security holes and probably
  7. # bugs in this code. If you test it please report ANY success or failure to
  8. # me (genone@gentoo.org).
  9.  
  10. # The following planned features are currently on hold:
  11. # - getting GLSAs from http/ftp servers (not really useful without the fixed ebuilds)
  12. # - GPG signing/verification (until key policy is clear)
  13.  
  14. __author__ = "Marius Mauch <genone@gentoo.org>"
  15.  
  16. import os, sys, urllib, time, string, codecs, re
  17. import xml.dom.minidom
  18.  
  19. if sys.version_info[0:2] < (2,3):
  20.     raise NotImplementedError("Python versions below 2.3 have broken XML code " \
  21.                                 +"and are not supported")
  22.  
  23. sys.path.insert(0, "/usr/lib/portage/pym")    # to find portage.py
  24.  
  25. import portage
  26.  
  27. # Note: the space for rgt and rlt is important !!
  28. opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=", 
  29.              "rge": ">=~", "rle": "<=~", "rgt": " >~", "rlt": " <~"}
  30. NEWLINE_ESCAPE = "!;\\n"    # some random string to mark newlines that should be preserved
  31. SPACE_ESCAPE = "!;_"        # some random string to mark spaces that should be preserved
  32.  
  33. def center(text, width):
  34.     """
  35.     Returns a string containing I{text} that is padded with spaces on both
  36.     sides. If C{len(text) >= width} I{text} is returned unchanged.
  37.     
  38.     @type    text: String
  39.     @param    text: the text to be embedded
  40.     @type    width: Integer
  41.     @param    width: the minimum length of the returned string
  42.     @rtype:        String
  43.     @return:    the expanded string or I{text}
  44.     """
  45.     if len(text) >= width:
  46.         return text
  47.     margin = (width-len(text))/2
  48.     rValue = " "*margin
  49.     rValue += text
  50.     if 2*margin + len(text) == width:
  51.         rValue += " "*margin
  52.     elif 2*margin + len(text) + 1 == width:
  53.         rValue += " "*(margin+1)
  54.     return rValue
  55.  
  56.  
  57. def wrap(text, width, caption=""):
  58.     """
  59.     Wraps the given text at column I{width}, optionally indenting
  60.     it so that no text is under I{caption}. It's possible to encode 
  61.     hard linebreaks in I{text} with L{NEWLINE_ESCAPE}.
  62.     
  63.     @type    text: String
  64.     @param    text: the text to be wrapped
  65.     @type    width: Integer
  66.     @param    width: the column at which the text should be wrapped
  67.     @type    caption: String
  68.     @param    caption: this string is inserted at the beginning of the 
  69.                      return value and the paragraph is indented up to
  70.                      C{len(caption)}.
  71.     @rtype:        String
  72.     @return:    the wrapped and indented paragraph
  73.     """
  74.     rValue = ""
  75.     line = caption
  76.     text = text.replace(2*NEWLINE_ESCAPE, NEWLINE_ESCAPE+" "+NEWLINE_ESCAPE)
  77.     words = text.split()
  78.     indentLevel = len(caption)+1
  79.     
  80.     for w in words:
  81.         if line[-1] == "\n":
  82.             rValue += line
  83.             line = " "*indentLevel
  84.         if len(line)+len(w.replace(NEWLINE_ESCAPE, ""))+1 > width:
  85.             rValue += line+"\n"
  86.             line = " "*indentLevel+w.replace(NEWLINE_ESCAPE, "\n")
  87.         elif w.find(NEWLINE_ESCAPE) >= 0:
  88.             if len(line.strip()) > 0:
  89.                 rValue += line+" "+w.replace(NEWLINE_ESCAPE, "\n")
  90.             else:
  91.                 rValue += line+w.replace(NEWLINE_ESCAPE, "\n")
  92.             line = " "*indentLevel
  93.         else:
  94.             if len(line.strip()) > 0:
  95.                 line += " "+w
  96.             else:
  97.                 line += w
  98.     if len(line) > 0:
  99.         rValue += line.replace(NEWLINE_ESCAPE, "\n")
  100.     rValue = rValue.replace(SPACE_ESCAPE, " ")
  101.     return rValue
  102.  
  103. def checkconfig(myconfig):
  104.     """
  105.     takes a portage.config instance and adds GLSA specific keys if
  106.     they are not present. TO-BE-REMOVED (should end up in make.*)
  107.     """
  108.     mysettings = {
  109.         "GLSA_DIR": portage.settings["PORTDIR"]+"/metadata/glsa/",
  110.         "GLSA_PREFIX": "glsa-",
  111.         "GLSA_SUFFIX": ".xml",
  112.         "CHECKFILE": "/var/cache/edb/glsa",
  113.         "GLSA_SERVER": "www.gentoo.org/security/en/glsa/",    # not completely implemented yet
  114.         "CHECKMODE": "local",                                # not completely implemented yet
  115.         "PRINTWIDTH": "76"
  116.     }
  117.     for k in mysettings.keys():
  118.         if not myconfig.has_key(k):
  119.             myconfig[k] = mysettings[k]
  120.     return myconfig
  121.  
  122. def get_glsa_list(repository, myconfig):
  123.     """
  124.     Returns a list of all available GLSAs in the given repository
  125.     by comparing the filelist there with the pattern described in
  126.     the config.
  127.     
  128.     @type    repository: String
  129.     @param    repository: The directory or an URL that contains GLSA files
  130.                         (Note: not implemented yet)
  131.     @type    myconfig: portage.config
  132.     @param    myconfig: a GLSA aware config instance (see L{checkconfig})
  133.     
  134.     @rtype:        List of Strings
  135.     @return:    a list of GLSA IDs in this repository
  136.     """
  137.     # TODO: remote fetch code for listing
  138.  
  139.     rValue = []
  140.  
  141.     if not os.access(repository, os.R_OK):
  142.         return []
  143.     dirlist = os.listdir(repository)
  144.     prefix = myconfig["GLSA_PREFIX"]
  145.     suffix = myconfig["GLSA_SUFFIX"]
  146.     
  147.     for f in dirlist:
  148.         try:
  149.             if f[:len(prefix)] == prefix:
  150.                 rValue.append(f[len(prefix):-1*len(suffix)])
  151.         except IndexError:
  152.             pass
  153.     return rValue
  154.  
  155. def getListElements(listnode):
  156.     """
  157.     Get all <li> elements for a given <ol> or <ul> node.
  158.     
  159.     @type    listnode: xml.dom.Node
  160.     @param    listnode: <ul> or <ol> list to get the elements for
  161.     @rtype:        List of Strings
  162.     @return:    a list that contains the value of the <li> elements
  163.     """
  164.     rValue = []
  165.     if not listnode.nodeName in ["ul", "ol"]:
  166.         raise GlsaFormatException("Invalid function call: listnode is not <ul> or <ol>")
  167.     for li in listnode.childNodes:
  168.         if li.nodeType != xml.dom.Node.ELEMENT_NODE:
  169.             continue
  170.         rValue.append(getText(li, format="strip"))
  171.     return rValue
  172.  
  173. def getText(node, format):
  174.     """
  175.     This is the main parser function. It takes a node and traverses
  176.     recursive over the subnodes, getting the text of each (and the
  177.     I{link} attribute for <uri> and <mail>). Depending on the I{format}
  178.     parameter the text might be formatted by adding/removing newlines,
  179.     tabs and spaces. This function is only useful for the GLSA DTD,
  180.     it's not applicable for other DTDs.
  181.     
  182.     @type    node: xml.dom.Node
  183.     @param    node: the root node to start with the parsing
  184.     @type    format: String
  185.     @param    format: this should be either I{strip}, I{keep} or I{xml}
  186.                     I{keep} just gets the text and does no formatting.
  187.                     I{strip} replaces newlines and tabs with spaces and
  188.                     replaces multiple spaces with one space.
  189.                     I{xml} does some more formatting, depending on the
  190.                     type of the encountered nodes.
  191.     @rtype:        String
  192.     @return:    the (formatted) content of the node and its subnodes
  193.     """
  194.     rValue = ""
  195.     if format in ["strip", "keep"]:
  196.         if node.nodeName in ["uri", "mail"]:
  197.             rValue += node.childNodes[0].data+": "+node.getAttribute("link")
  198.         else:
  199.             for subnode in node.childNodes:
  200.                 if subnode.nodeName == "#text":
  201.                     rValue += subnode.data
  202.                 else:
  203.                     rValue += getText(subnode, format)
  204.     else:
  205.         for subnode in node.childNodes:
  206.             if subnode.nodeName == "p":
  207.                 for p_subnode in subnode.childNodes:
  208.                     if p_subnode.nodeName == "#text":
  209.                         rValue += p_subnode.data.strip()
  210.                     elif p_subnode.nodeName in ["uri", "mail"]:
  211.                         rValue += p_subnode.childNodes[0].data
  212.                         rValue += " ( "+p_subnode.getAttribute("link")+" )"
  213.                 rValue += NEWLINE_ESCAPE
  214.             elif subnode.nodeName == "ul":
  215.                 for li in getListElements(subnode):
  216.                     rValue += "-"+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" "
  217.             elif subnode.nodeName == "ol":
  218.                 i = 0
  219.                 for li in getListElements(subnode):
  220.                     i = i+1
  221.                     rValue += str(i)+"."+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" "
  222.             elif subnode.nodeName == "code":
  223.                 rValue += getText(subnode, format="keep").replace("\n", NEWLINE_ESCAPE)
  224.                 if rValue[-1*len(NEWLINE_ESCAPE):] != NEWLINE_ESCAPE:
  225.                     rValue += NEWLINE_ESCAPE
  226.             elif subnode.nodeName == "#text":
  227.                 rValue += subnode.data
  228.             else:
  229.                 raise GlsaFormatException("Invalid Tag found: ", subnode.nodeName)
  230.     if format == "strip":
  231.         rValue = rValue.strip(" \n\t")
  232.         rValue = re.sub("[\s]{2,}", " ", rValue)
  233.     return str(rValue)
  234.  
  235. def getMultiTagsText(rootnode, tagname, format):
  236.     """
  237.     Returns a list with the text of all subnodes of type I{tagname}
  238.     under I{rootnode} (which itself is not parsed) using the given I{format}.
  239.     
  240.     @type    rootnode: xml.dom.Node
  241.     @param    rootnode: the node to search for I{tagname}
  242.     @type    tagname: String
  243.     @param    tagname: the name of the tags to search for
  244.     @type    format: String
  245.     @param    format: see L{getText}
  246.     @rtype:        List of Strings
  247.     @return:    a list containing the text of all I{tagname} childnodes
  248.     """
  249.     rValue = []
  250.     for e in rootnode.getElementsByTagName(tagname):
  251.         rValue.append(getText(e, format))
  252.     return rValue
  253.  
  254. def makeAtom(pkgname, versionNode):
  255.     """
  256.     creates from the given package name and information in the 
  257.     I{versionNode} a (syntactical) valid portage atom.
  258.     
  259.     @type    pkgname: String
  260.     @param    pkgname: the name of the package for this atom
  261.     @type    versionNode: xml.dom.Node
  262.     @param    versionNode: a <vulnerable> or <unaffected> Node that
  263.                          contains the version information for this atom
  264.     @rtype:        String
  265.     @return:    the portage atom
  266.     """
  267.     rValue = opMapping[versionNode.getAttribute("range")] \
  268.                 + pkgname \
  269.                 + "-" + getText(versionNode, format="strip")
  270.     return str(rValue)
  271.  
  272. def makeVersion(versionNode):
  273.     """
  274.     creates from the information in the I{versionNode} a 
  275.     version string (format <op><version>).
  276.     
  277.     @type    versionNode: xml.dom.Node
  278.     @param    versionNode: a <vulnerable> or <unaffected> Node that
  279.                          contains the version information for this atom
  280.     @rtype:        String
  281.     @return:    the version string
  282.     """
  283.     return opMapping[versionNode.getAttribute("range")] \
  284.             +getText(versionNode, format="strip")
  285.  
  286. def match(atom, portdbname):
  287.     """
  288.     wrapper that calls revisionMatch() or portage.dbapi.match() depending on 
  289.     the given atom.
  290.     
  291.     @type    atom: string
  292.     @param    atom: a <~ or >~ atom or a normal portage atom that contains the atom to match against
  293.     @type    portdb: portage.dbapi
  294.     @param    portdb:    one of the portage databases to use as information source
  295.     
  296.     @rtype:        list of strings
  297.     @return:    a list with the matching versions
  298.     """
  299.     if atom[2] == "~":
  300.         return revisionMatch(atom, portage.db["/"][portdbname].dbapi)
  301.     else:
  302.         return portage.db["/"][portdbname].dbapi.match(atom)
  303.  
  304. def revisionMatch(revisionAtom, portdb):
  305.     """
  306.     handler for the special >~, >=~, <=~ and <~ atoms that are supposed to behave
  307.     as > and < except that they are limited to the same version, the range only
  308.     applies to the revision part.
  309.     
  310.     @type    revisionAtom: string
  311.     @param    revisionAtom: a <~ or >~ atom that contains the atom to match against
  312.     @type    portdb: portage.dbapi
  313.     @param    portdb:    one of the portage databases to use as information source
  314.     
  315.     @rtype:        list of strings
  316.     @return:    a list with the matching versions
  317.     """
  318.     mylist = portdb.match(re.sub("-r[0-9]+$", "", revisionAtom[2:]))
  319.     rValue = []
  320.     for v in mylist:
  321.         r1 = portage.pkgsplit(v)[-1][1:]
  322.         r2 = portage.pkgsplit(revisionAtom[3:])[-1][1:]
  323.         if eval(r1+" "+revisionAtom[0:2]+" "+r2):
  324.             rValue.append(v)
  325.     return rValue
  326.         
  327.  
  328. def getMinUpgrade(vulnerableList, unaffectedList):
  329.     """
  330.     Checks if the systemstate is matching an atom in
  331.     I{vulnerableList} and returns string describing
  332.     the lowest version for the package that matches an atom in 
  333.     I{unaffectedList} and is greater than the currently installed
  334.     version or None if the system is not affected. Both
  335.     I{vulnerableList} and I{unaffectedList} should have the
  336.     same base package.
  337.     
  338.     @type    vulnerableList: List of Strings
  339.     @param    vulnerableList: atoms matching vulnerable package versions
  340.     @type    unaffectedList: List of Strings
  341.     @param    unaffectedList: atoms matching unaffected package versions
  342.     @rtype:        String | None
  343.     @return:    the lowest unaffected version that is greater than
  344.                 the installed version.
  345.     """
  346.     rValue = None
  347.     v_installed = []
  348.     u_installed = []
  349.     for v in vulnerableList:
  350.         v_installed += match(v, "vartree")
  351.  
  352.     for u in unaffectedList:
  353.         u_installed += match(u, "vartree")
  354.     
  355.     install_unaffected = True
  356.     for i in v_installed:
  357.         if i not in u_installed:
  358.             install_unaffected = False
  359.  
  360.     if install_unaffected:
  361.         return []
  362.     
  363.     for u in unaffectedList:
  364.         if u[2] == "~":
  365.             mylist = revisionMatch(u, portage.db["/"]["porttree"].dbapi)
  366.         else:
  367.             mylist = portage.db["/"]["porttree"].dbapi.xmatch("match-all", u)
  368.         for c in mylist:
  369.             c_pv = portage.catpkgsplit(c)
  370.             i_pv = portage.catpkgsplit(portage.best(v_installed))
  371.             if portage.pkgcmp(c_pv[1:], i_pv[1:]) > 0 and (rValue == None or portage.pkgcmp(c_pv[1:], portage.catpkgsplit(rValue)[1:]) < 0):
  372.                 rValue = c_pv[0]+"/"+c_pv[1]+"-"+c_pv[2]
  373.                 if c_pv[3] != "r0":        # we don't like -r0 for display
  374.                     rValue += "-"+c_pv[3]
  375.     return rValue
  376.  
  377. # simple Exception classes to catch specific errors
  378. class GlsaTypeException(Exception):
  379.     def __init__(self, doctype):
  380.         Exception.__init__(self, "wrong DOCTYPE: %s" % doctype)
  381.  
  382. class GlsaFormatException(Exception):
  383.     pass
  384.                 
  385. class GlsaArgumentException(Exception):
  386.     pass
  387.  
  388. # GLSA xml data wrapper class
  389. class Glsa:
  390.     """
  391.     This class is a wrapper for the XML data and provides methods to access
  392.     and display the contained data.
  393.     """
  394.     def __init__(self, myid, myconfig):
  395.         """
  396.         Simple constructor to set the ID, store the config and gets the 
  397.         XML data by calling C{self.read()}.
  398.         
  399.         @type    myid: String
  400.         @param    myid: String describing the id for the GLSA object (standard
  401.                       GLSAs have an ID of the form YYYYMM-nn) or an existing
  402.                       filename containing a GLSA.
  403.         @type    myconfig: portage.config
  404.         @param    myconfig: the config that should be used for this object.
  405.         """
  406.         if re.match(r'\d{6}-\d{2}', myid):
  407.             self.type = "id"
  408.         elif os.path.exists(myid):
  409.             self.type = "file"
  410.         else:
  411.             raise GlsaArgumentException("Given ID "+myid+" isn't a valid GLSA ID or filename.")
  412.         self.nr = myid
  413.         self.config = myconfig
  414.         self.read()
  415.  
  416.     def read(self):
  417.         """
  418.         Here we build the filename from the config and the ID and pass
  419.         it to urllib to fetch it from the filesystem or a remote server.
  420.         
  421.         @rtype:        None
  422.         @return:    None
  423.         """
  424.         if self.config["CHECKMODE"] == "local":
  425.             repository = "file://" + self.config["GLSA_DIR"]
  426.         else:
  427.             repository = self.config["GLSA_SERVER"]
  428.         if self.type == "file":
  429.             myurl = "file://"+self.nr
  430.         else:
  431.             myurl = repository + self.config["GLSA_PREFIX"] + str(self.nr) + self.config["GLSA_SUFFIX"]
  432.         self.parse(urllib.urlopen(myurl))
  433.         return None
  434.  
  435.     def parse(self, myfile):
  436.         """
  437.         This method parses the XML file and sets up the internal data 
  438.         structures by calling the different helper functions in this
  439.         module.
  440.         
  441.         @type    myfile: String
  442.         @param    myfile: Filename to grab the XML data from
  443.         @rtype:        None
  444.         @returns:    None
  445.         """
  446.         self.DOM = xml.dom.minidom.parse(myfile)
  447.         if not self.DOM.doctype:
  448.             raise GlsaTypeException(None)
  449.         elif self.DOM.doctype.systemId != "http://www.gentoo.org/dtd/glsa.dtd":
  450.             raise GlsaTypeException(self.DOM.doctype.systemId)
  451.         myroot = self.DOM.getElementsByTagName("glsa")[0]
  452.         if self.type == "id" and myroot.getAttribute("id") != self.nr:
  453.             raise GlsaFormatException("filename and internal id don't match:" + myroot.getAttribute("id") + " != " + self.nr)
  454.  
  455.         # the simple (single, required, top-level, #PCDATA) tags first
  456.         self.title = getText(myroot.getElementsByTagName("title")[0], format="strip")
  457.         self.synopsis = getText(myroot.getElementsByTagName("synopsis")[0], format="strip")
  458.         self.announced = getText(myroot.getElementsByTagName("announced")[0], format="strip")
  459.         self.revised = getText(myroot.getElementsByTagName("revised")[0], format="strip")
  460.         
  461.         # now the optional and 0-n toplevel, #PCDATA tags and references
  462.         try:
  463.             self.access = getText(myroot.getElementsByTagName("access")[0], format="strip")
  464.         except IndexError:
  465.             self.access = ""
  466.         self.bugs = getMultiTagsText(myroot, "bug", format="strip")
  467.         self.references = getMultiTagsText(myroot.getElementsByTagName("references")[0], "uri", format="keep")
  468.         
  469.         # and now the formatted text elements
  470.         self.description = getText(myroot.getElementsByTagName("description")[0], format="xml")
  471.         self.workaround = getText(myroot.getElementsByTagName("workaround")[0], format="xml")
  472.         self.resolution = getText(myroot.getElementsByTagName("resolution")[0], format="xml")
  473.         self.impact_text = getText(myroot.getElementsByTagName("impact")[0], format="xml")
  474.         self.impact_type = myroot.getElementsByTagName("impact")[0].getAttribute("type")
  475.         try:
  476.             self.background = getText(myroot.getElementsByTagName("background")[0], format="xml")
  477.         except IndexError:
  478.             self.background = ""                    
  479.  
  480.         # finally the interesting tags (product, affected, package)
  481.         self.glsatype = myroot.getElementsByTagName("product")[0].getAttribute("type")
  482.         self.product = getText(myroot.getElementsByTagName("product")[0], format="strip")
  483.         self.affected = myroot.getElementsByTagName("affected")[0]
  484.         self.packages = {}
  485.         for p in self.affected.getElementsByTagName("package"):
  486.             name = p.getAttribute("name")
  487.             if not self.packages.has_key(name):
  488.                 self.packages[name] = []
  489.             tmp = {}
  490.             tmp["arch"] = p.getAttribute("arch")
  491.             tmp["auto"] = (p.getAttribute("auto") == "yes")
  492.             tmp["vul_vers"] = [makeVersion(v) for v in p.getElementsByTagName("vulnerable")]
  493.             tmp["unaff_vers"] = [makeVersion(v) for v in p.getElementsByTagName("unaffected")]
  494.             tmp["vul_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("vulnerable")]
  495.             tmp["unaff_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("unaffected")]
  496.             self.packages[name].append(tmp)
  497.         # TODO: services aren't really used yet
  498.         self.services = self.affected.getElementsByTagName("service")
  499.         return None
  500.  
  501.     def dump(self, outstream=sys.stdout):
  502.         """
  503.         Dumps a plaintext representation of this GLSA to I{outfile} or 
  504.         B{stdout} if it is ommitted. You can specify an alternate
  505.         I{encoding} if needed (default is latin1).
  506.         
  507.         @type    outstream: File
  508.         @param    outfile: Stream that should be used for writing
  509.                          (defaults to sys.stdout)
  510.         """
  511.         width = int(self.config["PRINTWIDTH"])
  512.         outstream.write(center("GLSA %s: \n%s" % (self.nr, self.title), width)+"\n")
  513.         outstream.write((width*"=")+"\n")
  514.         outstream.write(wrap(self.synopsis, width, caption="Synopsis:         ")+"\n")
  515.         outstream.write("Announced on:      %s\n" % self.announced)
  516.         outstream.write("Last revised on:   %s\n\n" % self.revised)
  517.         if self.glsatype == "ebuild":
  518.             for k in self.packages.keys():
  519.                 pkg = self.packages[k]
  520.                 for path in pkg:
  521.                     vul_vers = string.join(path["vul_vers"])
  522.                     unaff_vers = string.join(path["unaff_vers"])
  523.                     outstream.write("Affected package:  %s\n" % k)
  524.                     outstream.write("Affected archs:    ")
  525.                     if path["arch"] == "*":
  526.                         outstream.write("All\n")
  527.                     else:
  528.                         outstream.write("%s\n" % path["arch"])
  529.                     outstream.write("Vulnerable:        %s\n" % vul_vers)
  530.                     outstream.write("Unaffected:        %s\n\n" % unaff_vers)
  531.         elif self.glsatype == "infrastructure":
  532.             pass
  533.         if len(self.bugs) > 0:
  534.             outstream.write("\nRelated bugs:      ")
  535.             for i in range(0, len(self.bugs)):
  536.                 outstream.write(self.bugs[i])
  537.                 if i < len(self.bugs)-1:
  538.                     outstream.write(", ")
  539.                 else:
  540.                     outstream.write("\n")                
  541.         if self.background:
  542.             outstream.write("\n"+wrap(self.background, width, caption="Background:       "))
  543.         outstream.write("\n"+wrap(self.description, width, caption="Description:      "))
  544.         outstream.write("\n"+wrap(self.impact_text, width, caption="Impact:           "))
  545.         outstream.write("\n"+wrap(self.workaround, width, caption="Workaround:       "))
  546.         outstream.write("\n"+wrap(self.resolution, width, caption="Resolution:       "))
  547.         myreferences = ""
  548.         for r in self.references:
  549.             myreferences += (r.replace(" ", SPACE_ESCAPE)+NEWLINE_ESCAPE+" ")
  550.         outstream.write("\n"+wrap(myreferences, width, caption="References:       "))
  551.         outstream.write("\n")
  552.     
  553.     def isVulnerable(self):
  554.         """
  555.         Tests if the system is affected by this GLSA by checking if any
  556.         vulnerable package versions are installed. Also checks for affected
  557.         architectures.
  558.         
  559.         @rtype:        Boolean
  560.         @returns:    True if the system is affected, False if not
  561.         """
  562.         vList = []
  563.         rValue = False
  564.         for k in self.packages.keys():
  565.             pkg = self.packages[k]
  566.             for path in pkg:
  567.                 if path["arch"] == "*" or self.config["ARCH"] in path["arch"].split():
  568.                     for v in path["vul_atoms"]:
  569.                         rValue = rValue \
  570.                             or (len(match(v, "vartree")) > 0 \
  571.                                 and getMinUpgrade(path["vul_atoms"], path["unaff_atoms"]))
  572.         return rValue
  573.     
  574.     def isApplied(self):
  575.         """
  576.         Looks if the GLSA IDis in the GLSA checkfile to check if this
  577.         GLSA was already applied.
  578.         
  579.         @rtype:        Boolean
  580.         @returns:    True if the GLSA was applied, False if not
  581.         """
  582.         aList = portage.grabfile(self.config["CHECKFILE"])
  583.         return (self.nr in aList)
  584.  
  585.     def inject(self):
  586.         """
  587.         Puts the ID of this GLSA into the GLSA checkfile, so it won't
  588.         show up on future checks. Should be called after a GLSA is 
  589.         applied or on explicit user request.
  590.  
  591.         @rtype:        None
  592.         @returns:    None
  593.         """
  594.         if not self.isApplied():
  595.             checkfile = open(self.config["CHECKFILE"], "a+")
  596.             checkfile.write(self.nr+"\n")
  597.             checkfile.close()
  598.         return None
  599.     
  600.     def getMergeList(self):
  601.         """
  602.         Returns the list of package-versions that have to be merged to
  603.         apply this GLSA properly. The versions are as low as possible 
  604.         while avoiding downgrades (see L{getMinUpgrade}).
  605.         
  606.         @rtype:        List of Strings
  607.         @return:    list of package-versions that have to be merged
  608.         """
  609.         rValue = []
  610.         for pkg in self.packages.keys():
  611.             for path in self.packages[pkg]:
  612.                 update = getMinUpgrade(path["vul_atoms"],
  613.                                         path["unaff_atoms"])
  614.                 if update:
  615.                     rValue.append(update)
  616.         return rValue
  617.